import { AppError, notFound } from "@autoscholar/core"; import { papers, paperVersions } from "@autoscholar/db"; import { publicArtifactKey } from "@autoscholar/storage"; import { and, eq } from "drizzle-orm"; import { handler } from "@/lib/server/api"; import { db, storage } from "@/lib/server/db"; export const runtime = "nodejs"; /** Public artifact delivery: only files listed in the published version's artifact set; immutable; integrity header. */ export const GET = handler(async (req, { params }) => { const { publicId, version, path } = (await params) as { publicId: string; version: string; path: string[] }; const vn = Number(version.replace(/^v/, "")); if (!Number.isInteger(vn) || vn < 1) throw notFound("version"); const rel = path.map(decodeURIComponent).join("/"); if (rel.includes("..") || rel.startsWith("/")) throw new AppError("invalid", "bad path", 400); const d = db(); const [row] = await d .select({ p: papers, v: paperVersions }) .from(papers) .innerJoin(paperVersions, and(eq(paperVersions.paperId, papers.id), eq(paperVersions.versionNo, vn))) .where(eq(papers.publicId, publicId.toUpperCase())) .limit(1); if (!row) throw notFound("paper version"); if (process.env.STAGING_MODE !== "1" && row.p.synthetic) throw notFound("paper version"); if ((row.v.metadata as { artifactsWithdrawn?: boolean }).artifactsWithdrawn) return new Response("Artifacts withdrawn. See the paper page for the notice.", { status: 410, headers: { "content-type": "text/plain" }, }); const entry = row.v.artifactSet.find((a) => a.path === rel); if (!entry) throw notFound("artifact"); const key = publicArtifactKey(row.p.publicId, vn, rel); const st = storage(); const head = await st.head(key); if (!head) throw notFound("artifact object"); if (head.sha256 !== entry.sha256) throw new AppError("integrity", "artifact integrity check failed", 500); const stream = await st.stream(key); const inline = /^(application\/pdf|image\/|text\/)/.test(entry.contentType) && new URL(req.url).searchParams.get("download") !== "1"; const filename = rel.split("/").pop() ?? "file"; const disp = `${inline ? "inline" : "attachment"}; filename="${row.p.publicId}-v${vn}-${filename}"`; const webStream = (await import("node:stream")).Readable.toWeb(stream) as ReadableStream; return new Response(webStream, { headers: { "content-type": entry.contentType, "content-length": String(entry.bytes), "content-disposition": disp, "x-artifact-sha256": entry.sha256, "cache-control": "public, max-age=31536000, immutable", "accept-ranges": "none", ...(entry.contentType.startsWith("text/") || entry.contentType === "application/json" ? { "content-security-policy": "sandbox" } : {}), }, }); });